﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using OpenTK;
using OpenTK.Graphics;
using OpenTK.Graphics.OpenGL;

namespace LasRendering
{
    public static class Graphics
    {
        #region Public properties

        public static bool AnimationEnabled
        {
            get;
            set;
        }

        public static AnimationMode AnimationMode
        {
            get;
            set;
        }

        private static int detailsLevel;
        public static int DetailsLevel
        {
            get
            {
                return detailsLevel;
            }
            set
            {
                detailsLevel = value % VertexBucket.NumBuckets;
                if (detailsLevel < 0)
                {
                    detailsLevel += VertexBucket.NumBuckets;
                }
            }
        }

        public static bool TwoPassesEnabled
        {
            get;
            set;
        }

        private static float cameraHeight;
        public static float CameraHeight
        {
            get
            {
                return cameraHeight;
            }
            set
            {
                cameraHeight = Math.Max(value, halfMaxHeight + 1);
            }
        }

        public static float CameraDistance
        {
            get;
            set;
        }

        public static float CameraRotationY
        {
            get;
            set;
        }

        public static float FovY
        {
            get;
            set;
        }

        public static Vector3 CenterPosition
        {
            get;
            set;
        }

        #endregion

        #region Private fields

        private static float reloadFactor;
        private static float pointSizeScale;
        private static float vertexAvgSize;
        private static float diagonalAngle;
        private static float halfMaxHeight;
        private static float maxDistance;
        private static float depthEpsilonCoefficient; // coefficient for the average vertex size
        private static WallDrawingMethod wallDrawingMethod;
        private static Vector3 lightPosition;
        private static Vector3 lightColor;
        private static Vector3 globalAmbient;
        private static Vector3 Ka;        //brassAmbient
        private static Vector3 Kd;        //brassDiffuse
        private static Vector3 Ke;        //brassEmissive
        private static Vector3 Ks;        //brassSpecular
        private static float shininess;   //brassShininess
        private static Color4 backgroundColor;

        private static QuadTree vertexQuadTree;
        private static PriorityQueue<float, VisibleQuadTreeNode> visibleQueue;
        private static List<VisibleQuadTreeNode> orphanVisibleList;
        private static object visibleQueueLock = new object();
        private static Thread loadVertexThread;
        private static float numOfExpectedPointMultiplier;

        // MRT: GL_COLOR_ATTACHMENT0_EXT, GL_COLOR_ATTACHMENT1_EXT and GL_DEPTH_ATTACHMENT_EXT
        // GL_COLOR_ATTACHMENT0_EXT:  xyz: color   a: 0 discard; >0 alpha
        // GL_COLOR_ATTACHMENT1_EXT:  xyz: normal  w: weight
        private static int mrtFboId;
        private static DrawBuffersEnum[] mrtTextures = { (DrawBuffersEnum)FramebufferAttachment.ColorAttachment0Ext, (DrawBuffersEnum)FramebufferAttachment.ColorAttachment1Ext };
        private static int[] mrtTexturesId = new int[mrtTextures.Length];
        private static int mrtDepthTextureId;

        // Compute Gaussian value in shader is slightly faster than reading gaussian texture, (USC fps 15.5 vs 14.9), so we didin't use it.
        private static int gauss1DTextureId;
        private static int gauss2DTextureId;

        private static IntPtr vpVertex;
        private static IntPtr fpVertex;
        private static IntPtr fpVertexDepthPass;
        private static IntPtr vpWall;
        private static IntPtr gpWall;
        private static IntPtr fpWall;
        private static IntPtr fpWallDepthPass;
        private static IntPtr vpLighting;
        private static IntPtr fpLighting;

        #endregion

        #region Init/Uninit

        static Graphics()
        {
            AnimationEnabled = false;
            DetailsLevel = VertexBucket.NumBuckets - 1;
            TwoPassesEnabled = true;
        }

        public static void Init(int windowWidth, int windowHeight, QuadTree quadTree)
        {
            // Enlarge the point size a little bit to make sure it will cover the screen.
            pointSizeScale = Convert.ToSingle(ConfigurationManager.AppSettings["pointSizeScale"]);
            vertexAvgSize = quadTree.VertexAvgSize * pointSizeScale;

            reloadFactor = Convert.ToSingle(ConfigurationManager.AppSettings["reloadFactor"]);
            depthEpsilonCoefficient = Convert.ToSingle(ConfigurationManager.AppSettings["depthEpsilonCoefficient"]);
            wallDrawingMethod = (WallDrawingMethod)Enum.Parse(typeof(WallDrawingMethod), ConfigurationManager.AppSettings["wallDrawingMethod"], true);
            var bgColor = VectorHelper.Parse(ConfigurationManager.AppSettings["backgroundColor"]);
            backgroundColor = new Color4(bgColor.X, bgColor.Y, bgColor.Z, 0);
            AnimationMode = (AnimationMode)Enum.Parse(typeof(AnimationMode), ConfigurationManager.AppSettings["animationMode"], true);
            numOfExpectedPointMultiplier = Convert.ToSingle(ConfigurationManager.AppSettings["numOfExpectedPointMultiplier"]);

            //Set camera and scene settings
            maxDistance = (quadTree.MaxPosition - quadTree.MinPosition).Length;
            CenterPosition = (quadTree.MinPosition + quadTree.MaxPosition) / 2;
            diagonalAngle = (float)Math.Atan(Math.Abs(CenterPosition.Z / CenterPosition.X));
            halfMaxHeight = (quadTree.MaxPosition.Y - quadTree.MinPosition.Y) / 2f;
            CameraDistance = maxDistance / 2;
            CameraHeight = CameraDistance * (float)Math.Tan(MathHelper.DegreesToRadians(15));
            CameraRotationY = 0;
            FovY = Convert.ToSingle(ConfigurationManager.AppSettings["fovY"]);

            lightPosition = new Vector3(quadTree.MinPosition.X, quadTree.MaxPosition.Y * 2, quadTree.MinPosition.Z);
            lightColor = VectorHelper.Parse(ConfigurationManager.AppSettings["lightColor"]);
            globalAmbient = VectorHelper.Parse(ConfigurationManager.AppSettings["globalAmbient"]);
            Ka = VectorHelper.Parse(ConfigurationManager.AppSettings["Ka"]);
            Kd = VectorHelper.Parse(ConfigurationManager.AppSettings["Kd"]);
            Ke = VectorHelper.Parse(ConfigurationManager.AppSettings["Ke"]);
            Ks = VectorHelper.Parse(ConfigurationManager.AppSettings["Ks"]);
            shininess = Convert.ToSingle(ConfigurationManager.AppSettings["shininess"]);

            //Set window size
            GL.Viewport(0, 0, windowWidth, windowHeight);

            //Initialize MRT
            InitMrt(windowWidth, windowHeight);

            //Initialize textures for Gaussian function
            InitGaussTextures();

            //Initialize shaders
            ShaderHelper.InitCg();
            vpVertex = ShaderHelper.LoadProgram(ShaderHelper.VertexProfile, "vpVertex.cg");
            fpVertex = ShaderHelper.LoadProgram(ShaderHelper.FragmentProfile, "fpVertex.cg");
            fpVertexDepthPass = ShaderHelper.LoadProgram(ShaderHelper.FragmentProfile, "fpVertex.cg", "main_depthPass");
            if (wallDrawingMethod != WallDrawingMethod.Points)
            {
                vpWall = ShaderHelper.LoadProgram(ShaderHelper.VertexProfile, "vpWall.cg");
                gpWall = ShaderHelper.LoadProgram(ShaderHelper.GeometryProfile, string.Format("gpWall-{0}.cg", wallDrawingMethod));
                fpWall = ShaderHelper.LoadProgram(ShaderHelper.FragmentProfile, string.Format("fpWall-{0}.cg", wallDrawingMethod));
                fpWallDepthPass = ShaderHelper.LoadProgram(ShaderHelper.FragmentProfile, string.Format("fpWall-{0}.cg", wallDrawingMethod), "main_depthPass");
            }
            vpLighting = ShaderHelper.LoadProgram(ShaderHelper.VertexProfile, "vpLighting.cg");
            fpLighting = ShaderHelper.LoadProgram(ShaderHelper.FragmentProfile, "fpLighting.cg");
            if (wallDrawingMethod != WallDrawingMethod.Points)
            {
                ShaderHelper.SetParameter(gpWall, "vertexAvgSize", vertexAvgSize);
            }
            ShaderHelper.SetTextureParameter(fpLighting, "colorTexture", mrtTexturesId[0]);
            ShaderHelper.SetTextureParameter(fpLighting, "normalTexture", mrtTexturesId[1]);
            ShaderHelper.SetTextureParameter(fpLighting, "depthTexture", mrtDepthTextureId);
            ShaderHelper.SetParameter(fpLighting, "lightPosition", lightPosition);
            ShaderHelper.SetParameter(fpLighting, "lightColor", lightColor);
            ShaderHelper.SetParameter(fpLighting, "globalAmbient", globalAmbient);
            ShaderHelper.SetParameter(fpLighting, "Ka", Ka);
            ShaderHelper.SetParameter(fpLighting, "Kd", Kd);
            ShaderHelper.SetParameter(fpLighting, "Ke", Ke);
            ShaderHelper.SetParameter(fpLighting, "Ks", Ks);
            ShaderHelper.SetParameter(fpLighting, "shininess", shininess);

            //Global OpenGL state change
            GL.Enable(EnableCap.PointSprite);
            GL.Enable(EnableCap.VertexProgramPointSize);
            GL.TexEnv(TextureEnvTarget.PointSprite, TextureEnvParameter.CoordReplace, (int)All.True);
            GL.DepthFunc(DepthFunction.Lequal);
            GL.DepthMask(true);
            GL.BlendEquation(BlendEquationMode.FuncAdd);
            GL.BlendFunc(BlendingFactorSrc.One, BlendingFactorDest.One);

            //Save quadTree, queue, and start a thread
            vertexQuadTree = quadTree;
            visibleQueue = new PriorityQueue<float, VisibleQuadTreeNode>();
            orphanVisibleList = new List<VisibleQuadTreeNode>();
            loadVertexThread = new Thread(new ThreadStart(LoadVisibleVertexThreadFunc));
            loadVertexThread.Start();
        }

        private static void InitMrt(int windowWidth, int windowHeight)
        {
            int maxNumMrtTextures;
            GL.GetInteger(GetPName.MaxColorAttachmentsExt, out maxNumMrtTextures);
            Debug.Assert(maxNumMrtTextures >= mrtTextures.Length);

            // These 3 lines will prevent OpenGL to clamp color values for MRT.
            // Because total weight might be greater than 1, we need to disable clamp.
            // If want to enable clamp, change MRT texture internal format to from GL_RGBA16F_ARB/GL_RGBA32F_ARB to GL_RGBA at glTexImage2D.
            // GL_RGBA is default texture internal format value, which isn't compatible with disabling clamp.
            GL.Arb.ClampColor(ArbColorBufferFloat.ClampVertexColorArb, (ArbColorBufferFloat)All.False);
            GL.Arb.ClampColor(ArbColorBufferFloat.ClampReadColorArb, (ArbColorBufferFloat)All.False);
            GL.Arb.ClampColor(ArbColorBufferFloat.ClampFragmentColorArb, (ArbColorBufferFloat)All.False);

            // Create an FBO
            GL.Ext.GenFramebuffers(1, out mrtFboId);

            // Create MRT textures
            GL.GenTextures(mrtTexturesId.Length, mrtTexturesId);
            for (int i = 0; i < mrtTexturesId.Length; i++)
            {
                GL.BindTexture(TextureTarget.Texture2D, mrtTexturesId[i]);
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
                GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
                // Use GL_RGBA16F_ARB to speed up renderring.
                GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.Rgba16f, windowWidth, windowHeight, 0, PixelFormat.Rgba, PixelType.Float, IntPtr.Zero);
                GL.BindTexture(TextureTarget.Texture2D, 0);
            }

            GL.GenTextures(1, out mrtDepthTextureId);
            GL.BindTexture(TextureTarget.Texture2D, mrtDepthTextureId);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.DepthTextureMode, (int)All.Intensity);
            GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.DepthComponent24, windowWidth, windowHeight, 0, PixelFormat.DepthComponent, PixelType.Float, IntPtr.Zero);
            GL.BindTexture(TextureTarget.Texture2D, 0);

            // Bind the FBO and attach color texture to it
            GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, mrtFboId);
            for (int i = 0; i < mrtTexturesId.Length; i++)
            {
                GL.Ext.FramebufferTexture2D(FramebufferTarget.FramebufferExt, (FramebufferAttachment)mrtTextures[i], TextureTarget.Texture2D, mrtTexturesId[i], 0);
            }
            GL.Ext.FramebufferTexture2D(FramebufferTarget.FramebufferExt, FramebufferAttachment.DepthAttachmentExt, TextureTarget.Texture2D, mrtDepthTextureId, 0);
            GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0);
        }

        private static void InitGaussTextures()
        {
            //http://en.wikipedia.org/wiki/Gaussian_function

            const int gaussTextureWidth = 128;
            float[] buf1D = new float[gaussTextureWidth];
            float[,] buf2D = new float[gaussTextureWidth, gaussTextureWidth];
            GCHandle handle;

            const float x0 = 0.5f, y0 = 0.5f, A = 1, sigma = 0.2f;
            for (int i = 0; i < gaussTextureWidth; i++)
            {
                double x = (i + 0.5f) / gaussTextureWidth;
                double numX = (x - x0) / sigma;
                numX = numX * numX / 2;
                buf1D[i] = (float)(A * Math.Exp(-numX));

                for (int j = 0; j < gaussTextureWidth; j++)
                {
                    double y = (j + 0.5f) / gaussTextureWidth;
                    double numY = (y - y0) / sigma;
                    numY = numY * numY / 2;
                    buf2D[i, j] = (float)(A * Math.Exp(-(numX + numY)));
                }
            }

            GL.GenTextures(1, out gauss1DTextureId);
            GL.BindTexture(TextureTarget.Texture1D, gauss1DTextureId);
            GL.TexParameter(TextureTarget.Texture1D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
            GL.TexParameter(TextureTarget.Texture1D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
            handle = GCHandle.Alloc(buf1D, GCHandleType.Pinned);
            GL.TexImage1D(TextureTarget.Texture1D, 0, PixelInternalFormat.One, gaussTextureWidth, 0, PixelFormat.Red, PixelType.Float, handle.AddrOfPinnedObject());
            handle.Free();
            GL.BindTexture(TextureTarget.Texture1D, 0);

            GL.GenTextures(1, out gauss2DTextureId);
            GL.BindTexture(TextureTarget.Texture2D, gauss2DTextureId);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
            GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
            handle = GCHandle.Alloc(buf2D, GCHandleType.Pinned);
            GL.TexImage2D(TextureTarget.Texture2D, 0, PixelInternalFormat.One, gaussTextureWidth, gaussTextureWidth, 0, PixelFormat.Red, PixelType.Float, handle.AddrOfPinnedObject());
            handle.Free();
            GL.BindTexture(TextureTarget.Texture2D, 0);
        }

        public static void Uninit()
        {
            var visibleQueuePendingDelete = visibleQueue;
            visibleQueue = null; //signal the thread to exit.
            loadVertexThread.Join();
            foreach (var node in visibleQueuePendingDelete)
            {
                Free(node);
            }
            visibleQueuePendingDelete = null;
            foreach (var node in orphanVisibleList)
            {
                Free(node);
            }
            orphanVisibleList.Clear();

            ShaderHelper.UnloadProgram(vpVertex);
            ShaderHelper.UnloadProgram(fpVertex);
            ShaderHelper.UnloadProgram(fpVertexDepthPass);
            if (wallDrawingMethod != WallDrawingMethod.Points)
            {
                ShaderHelper.UnloadProgram(vpWall);
                ShaderHelper.UnloadProgram(gpWall);
                ShaderHelper.UnloadProgram(fpWall);
                ShaderHelper.UnloadProgram(fpWallDepthPass);
            }
            ShaderHelper.UnloadProgram(vpLighting);
            ShaderHelper.UnloadProgram(fpLighting);
            ShaderHelper.UninitCg();

            GL.DeleteTextures(1, ref gauss1DTextureId);
            GL.DeleteTextures(1, ref gauss2DTextureId);
            GL.DeleteTextures(mrtTexturesId.Length, mrtTexturesId);
            GL.DeleteTextures(1, ref mrtDepthTextureId);
            GL.Ext.DeleteFramebuffers(1, ref mrtFboId);
        }

        private static void Free(VisibleQuadTreeNode node)
        {
            if (node.VertexBuckets != null)
            {
                for (int i = 0; i < VertexBucket.NumBuckets; i++)
                {
                    var vertexBucket = node.VertexBuckets[i];
                    if (vertexBucket.VertexBufferObjectId > 0)
                    {
                        GL.DeleteBuffers(1, ref vertexBucket.VertexBufferObjectId);
                        vertexBucket.VertexBufferObjectId = 0;
                    }
                    if (vertexBucket.Ptr != IntPtr.Zero)
                    {
                        VertexHelper.FreeMemory(vertexBucket);
                    }
                }
                node.VertexBuckets = null;
            }
        }

        #endregion

        #region Update visible queue

        public static void UpdateVisibleQueue(Vector3 eyePosition, Matrix4 modelViewProjMatrix, int windowWidth, int windowHeight)
        {
            int numOfPixelsOnWindow = windowWidth * windowHeight;

            lock (visibleQueueLock)
            {
                visibleQueue = new PriorityQueue<float, VisibleQuadTreeNode>();

                vertexQuadTree.EnumerateVisibleNode(
                    delegate(Vector2 position)
                    {
                        var p = new Vector4(position.X, CenterPosition.Y, -position.Y, 1);
                        float t = Vector4.Dot(modelViewProjMatrix.Column3, p);
                        float u = Vector4.Dot(modelViewProjMatrix.Column0, p) / t;
                        float v = Vector4.Dot(modelViewProjMatrix.Column1, p) / t;
                        return new Vector2((u + 1) / 2, (v + 1) / 2);
                    },
                    delegate(QuadTreeNode node, Vector2 uv0, Vector2 uv1, Vector2 uv2, Vector2 uv3)
                    {
                        // Using Heron's formula to calculate the area covered on screen.
                        float a = (uv0 - uv1).Length;
                        float b = (uv1 - uv3).Length;
                        float c = (uv3 - uv2).Length;
                        float d = (uv2 - uv0).Length;
                        float e = (uv0 - uv3).Length;
                        float p = (a + b + e) / 2;
                        float q = (c + d + e) / 2;
                        float area = (float)(Math.Sqrt(p * (p - a) * (p - b) * (p - e)) + Math.Sqrt(q * (q - c) * (q - d) * (q - e)));
                        area = Math.Min(area, 1);

                        var priority = (new Vector3(node.CenterPosition.X, CenterPosition.Y, -node.CenterPosition.Y) - eyePosition).LengthFast;
                        var expectedNumOfVertices = (int)Math.Min((long)(area * numOfPixelsOnWindow * numOfExpectedPointMultiplier), (long)int.MaxValue);
                        bool createNewNode;
                        VisibleQuadTreeNode orphanNode = null;

                        if (node.VisibleNode == null)
                        {
                            createNewNode = true;
                        }
                        else
                        {
                            createNewNode = false;
                            var visibleNode = node.VisibleNode;
                            if (visibleNode.Status != VisibilityStatus.Visible)
                            {
                                createNewNode = true;
                            }
                            else if (expectedNumOfVertices > node.VisibleNode.ExpectedNumOfVertices * reloadFactor ||
                                expectedNumOfVertices < node.VisibleNode.ExpectedNumOfVertices / reloadFactor)
                            {
                                createNewNode = true;
                            }

                            if (createNewNode)
                            {
                                visibleNode.Data = null;
                                node.VisibleNode = null;
                                orphanVisibleList.Add(visibleNode);
                                orphanNode = visibleNode;
                            }
                            else
                            {
                                visibleNode.Priority = priority;
                                visibleQueue.Push(priority, visibleNode);
                            }
                        }

                        if (createNewNode)
                        {
                            var visibleNode = new VisibleQuadTreeNode();
                            visibleNode.Data = node;
                            visibleNode.Status = VisibilityStatus.Visible;
                            visibleNode.Priority = priority;
                            visibleNode.ExpectedNumOfVertices = expectedNumOfVertices;
                            visibleNode.Override = orphanNode;
                            node.VisibleNode = visibleNode;
                            visibleQueue.Push(priority, visibleNode);
                        }
                    },
                    delegate(QuadTreeNode node)
                    {
                        if (node.VisibleNode != null)
                        {
                            node.VisibleNode.Status = VisibilityStatus.Invisible;
                            visibleQueue.Push(float.MaxValue, node.VisibleNode);
                        }
                    });

                for (int i = orphanVisibleList.Count - 1; i >= 0; i--)
                {
                    var visibleNode = orphanVisibleList[i];
                    if (visibleNode.Status != VisibilityStatus.Deleted)
                    {
                        visibleQueue.Push(visibleNode.Priority, visibleNode);
                    }
                    else
                    {
                        orphanVisibleList.RemoveAt(i);
                    }
                }
            }
        }

        public static void UpdateVboInVisibleQueue()
        {
            //Do NOT update the queue itself.
            foreach (var node in visibleQueue)
            {
                if (node.Status == VisibilityStatus.Visible)
                {
                    if (node.VertexBuckets != null)
                    {
                        for (int i = 0; i < VertexBucket.NumBuckets; i++)
                        {
                            var vertexBucket = node.VertexBuckets[i];
                            if (vertexBucket.Ptr != IntPtr.Zero)
                            {
                                Debug.Assert(vertexBucket.VertexBufferObjectId == 0);
                                if (vertexBucket.VertexBufferObjectId > 0)
                                {
                                    GL.DeleteBuffers(1, ref vertexBucket.VertexBufferObjectId);
                                    vertexBucket.VertexBufferObjectId = 0;
                                }

                                GL.GenBuffers(1, out vertexBucket.VertexBufferObjectId);
                                GL.BindBuffer(BufferTarget.ArrayBuffer, vertexBucket.VertexBufferObjectId);
                                GL.BufferData(BufferTarget.ArrayBuffer, new IntPtr(vertexBucket.BytesPerVertex * vertexBucket.CountInMemory), vertexBucket.Ptr, BufferUsageHint.StaticDraw);
                                vertexBucket.CountInVBO = vertexBucket.CountInMemory;
                                VertexHelper.FreeMemory(vertexBucket);
                            }
                        }
                        if (node.Override != null && node.Override.Status == VisibilityStatus.Visible)
                        {
                            node.Override.Status = VisibilityStatus.Invisible;
                            node.Override = null;
                        }
                    }
                }
                else if (node.Status == VisibilityStatus.Invisible)
                {
                    if (node.VertexBuckets != null)
                    {
                        for (int i = 0; i < VertexBucket.NumBuckets; i++)
                        {
                            var vertexBucket = node.VertexBuckets[i];
                            if (vertexBucket.VertexBufferObjectId > 0)
                            {
                                GL.DeleteBuffers(1, ref vertexBucket.VertexBufferObjectId);
                                vertexBucket.VertexBufferObjectId = 0;
                            }
                        }
                    }
                    node.Status = VisibilityStatus.PendingDelete;
                }
            }
        }

        private static void LoadVisibleVertexThreadFunc()
        {
            while (visibleQueue != null)
            {
                VisibleQuadTreeNode nodePendingUpdate = null;
                int updateScenario = 0;

                lock (visibleQueueLock)
                {
                    Debug.Assert(visibleQueue != null);

                    foreach (var node in visibleQueue)
                    {
                        if (node.Status == VisibilityStatus.Visible)
                        {
                            if (node.VertexBuckets == null)
                            {
                                // To load new node
                                nodePendingUpdate = node;
                                updateScenario = 1;
                                break;
                            }
                        }
                        else if (node.Status == VisibilityStatus.PendingDelete)
                        {
                            // To unload existing node.
                            nodePendingUpdate = node;
                            updateScenario = 2;
                            break;
                        }
                    }
                }

                switch (updateScenario)
                {
                    case 1:
                        if (nodePendingUpdate.Data != null)
                        {
                            // We just read vertices from file here, cannot call glGenBuffer because this is not rendering thread.
                            nodePendingUpdate.VertexBuckets = VertexHelper.CreateVertexBuckets(vertexQuadTree, nodePendingUpdate.Data, nodePendingUpdate.ExpectedNumOfVertices);
                        }
                        else
                        {
                            goto case 2;
                        }
                        break;

                    case 2:
                        if (nodePendingUpdate.Data != null && nodePendingUpdate.Data.VisibleNode == nodePendingUpdate)
                        {
                            nodePendingUpdate.Data.VisibleNode = null;
                        }
                        nodePendingUpdate.VertexBuckets = null;
                        nodePendingUpdate.Status = VisibilityStatus.Deleted;

                        if (nodePendingUpdate.Override != null && nodePendingUpdate.Override.Status == VisibilityStatus.Visible)
                        {
                            nodePendingUpdate.Override.Status = VisibilityStatus.Invisible;
                            nodePendingUpdate.Override = null;
                        }
                        break;

                    default:
                        Debug.Assert(updateScenario == 0);
                        break;
                }

                if (nodePendingUpdate == null)
                {
                    Thread.Sleep(50);
                }
                else
                {
                    Thread.Sleep(1);
                }
            }
        }

        #endregion

        #region Rendering

        public static void RenderFrame(int windowWidth, int windowHeight, out int vertexCount)
        {
            Vector3 eyePosition;
            Vector3 lookAtPosition;
            ComputeViewParameters(out eyePosition, out lookAtPosition);

            float distanceFromEyeToCenter = (float)Math.Sqrt(CameraDistance * CameraDistance + CameraHeight * CameraHeight);
            float halfMaxDistance = maxDistance / 2;
            float nearCull = Math.Max(1, Math.Max(distanceFromEyeToCenter - halfMaxDistance, (CameraHeight - halfMaxHeight) * 0.9f));
            float farCull = distanceFromEyeToCenter + halfMaxDistance;
            float depthOffset = depthEpsilonCoefficient * vertexAvgSize / (farCull - nearCull);
            Matrix4 perspectiveMatrix = Matrix4.CreatePerspectiveFieldOfView(
                MathHelper.DegreesToRadians(FovY), (float)windowWidth / windowHeight,
                nearCull, farCull);
            Matrix4 lookAtMatrix = Matrix4.LookAt(
                eyePosition.X, eyePosition.Y, eyePosition.Z,
                lookAtPosition.X, lookAtPosition.Y, lookAtPosition.Z,
                0, 1, 0);
            Matrix4 modelViewProjMatrix = lookAtMatrix * perspectiveMatrix;
            Matrix4 modelViewProjMatrixInv = modelViewProjMatrix;
            modelViewProjMatrixInv.Invert();

            //TODO: TEMP
            UpdateVisibleQueue(eyePosition, modelViewProjMatrix, windowWidth, windowHeight);
            UpdateVboInVisibleQueue();

            GL.MatrixMode(MatrixMode.Projection);
            GL.LoadMatrix(ref perspectiveMatrix);
            GL.MatrixMode(MatrixMode.Modelview);
            GL.LoadMatrix(ref lookAtMatrix);

            //Set shader parameters
            ShaderHelper.SetParameter(vpVertex, "eyePosition", eyePosition);
            ShaderHelper.SetParameter(vpVertex, "nearCull", nearCull);
            ShaderHelper.SetParameter(vpVertex, "farNearDistance", farCull - nearCull);
            ShaderHelper.SetParameter(vpVertex, "modelViewProj", modelViewProjMatrix);
            ShaderHelper.SetParameter(fpVertexDepthPass, "depthOffset", depthOffset);
            if (wallDrawingMethod != WallDrawingMethod.Points)
            {
                ShaderHelper.SetParameter(gpWall, "eyePosition", eyePosition);
                ShaderHelper.SetParameter(gpWall, "nearCull", nearCull);
                ShaderHelper.SetParameter(gpWall, "farNearDistance", farCull - nearCull);
                ShaderHelper.SetParameter(gpWall, "modelViewProj", modelViewProjMatrix);
                ShaderHelper.SetParameter(fpWallDepthPass, "depthOffset", depthOffset);
            }
            ShaderHelper.SetParameter(fpLighting, "eyePosition", eyePosition);
            ShaderHelper.SetParameter(fpLighting, "nearCull", nearCull);
            ShaderHelper.SetParameter(fpLighting, "farNearDistance", farCull - nearCull);
            ShaderHelper.SetParameter(fpLighting, "modelViewProjInv", modelViewProjMatrixInv);

            //Draw Points to MRT FBO
            GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, mrtFboId);
            GL.DrawBuffers(mrtTextures.Length, mrtTextures);

            //Enable depth test.
            //Updating depth buffer was enabled by default in Init().
            //Depth function was set to Lequal in Init(), otherwise, if using 2 passes and depthOffset is 0, we got nothing on some pixels.
            GL.Enable(EnableCap.DepthTest);

            //Clear everything, must run after enable depth, otherwise, depth buffer won't clear.
            GL.ClearColor(0, 0, 0, 0);
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

            vertexCount = 0;

            if (!TwoPassesEnabled)
            {
                DrawVertices(VertexBucket.WallBucketId, false, ref vertexCount);
                DrawVertices(VertexBucket.RoofBucketId, false, ref vertexCount);
                DrawVertices(VertexBucket.GrndBucketId, false, ref vertexCount);
            }
            else
            {
                //Pass #1
                //Draw to no color buffer (but still  have depth buffer), to speed up.
                GL.DrawBuffers(0, mrtTextures);
                //If there's color buffer, we need to clear color buffer only, keep depth buffer.
                //GL.glClear(GL.GL_COLOR_BUFFER_BIT);

                int foo = 0;
                DrawVertices(VertexBucket.WallBucketId, true, ref foo);
                DrawVertices(VertexBucket.RoofBucketId, true, ref foo);
                DrawVertices(VertexBucket.GrndBucketId, true, ref foo);

                //Pass #2
                GL.DrawBuffers(mrtTextures.Length, mrtTextures);
                //Don't clear, keep everything.

                //Stop updating depth buffer.
                GL.DepthMask(false);
                //Set blend function to accumulate colors weigthed by alpha channel, use pre-multiplied alpha color.
                //Blend equation was set to FuncAdd in Init().
                //Blend func was set to ONE-ONE in Init(). ONE-ONE func will sum-up all values, we will divide them by total-weight in fragment shader to get weighted average.
                GL.Enable(EnableCap.Blend);

                DrawVertices(VertexBucket.WallBucketId, false, ref vertexCount);
                DrawVertices(VertexBucket.RoofBucketId, false, ref vertexCount);
                DrawVertices(VertexBucket.GrndBucketId, false, ref vertexCount);

                GL.Disable(EnableCap.Blend);
                GL.DepthMask(true);
            }

            //Finally draw vertices which never needs 2 passes
            //Trees, don't need 2 passes.
            DrawVertices(VertexBucket.TreeBucketId, false, ref vertexCount);

            GL.Disable(EnableCap.DepthTest);

            //Draw to screen (back)
            GL.Ext.BindFramebuffer(FramebufferTarget.FramebufferExt, 0);

            GL.ClearColor(backgroundColor);
            GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);

            ShaderHelper.BindProgram(ShaderHelper.VertexProfile, vpLighting);
            ShaderHelper.BindProgram(ShaderHelper.FragmentProfile, fpLighting);

            GL.Begin(BeginMode.Quads);
            {
                GL.Vertex2(0, 0);
                GL.Vertex2(1, 0);
                GL.Vertex2(1, 1);
                GL.Vertex2(0, 1);
            }
            GL.End();

            ShaderHelper.UnbindProgram(ShaderHelper.VertexProfile);
            ShaderHelper.UnbindProgram(ShaderHelper.FragmentProfile);
        }

        private static void DrawVertices(int bucketId, bool depthPass, ref int vertexCount)
        {
            if (bucketId > DetailsLevel)
            {
                return;
            }

            bool vpVertexBinded = false;
            if (bucketId == VertexBucket.WallBucketId && wallDrawingMethod != WallDrawingMethod.Points)
            {
                ShaderHelper.BindProgram(ShaderHelper.VertexProfile, vpWall);
                ShaderHelper.BindProgram(ShaderHelper.GeometryProfile, gpWall);
                if (depthPass)
                {
                    ShaderHelper.BindProgram(ShaderHelper.FragmentProfile, fpWallDepthPass);
                }
                else
                {
                    ShaderHelper.BindProgram(ShaderHelper.FragmentProfile, fpWall);
                }
            }
            else
            {
                vpVertexBinded = true;
                ShaderHelper.BindProgram(ShaderHelper.VertexProfile, vpVertex);
                ShaderHelper.SetParameter(vpVertex, "backFaceCulling", bucketId == VertexBucket.RoofBucketId ? 1 : 0);
                if (depthPass)
                {
                    ShaderHelper.BindProgram(ShaderHelper.FragmentProfile, fpVertexDepthPass);
                }
                else
                {
                    ShaderHelper.BindProgram(ShaderHelper.FragmentProfile, fpVertex);
                }
            }

            foreach (var node in visibleQueue)
            {
                if (node.Status == VisibilityStatus.Visible && node.VertexBuckets != null)
                {
                    var vertexBucket = node.VertexBuckets[bucketId];
                    if (vertexBucket.VertexBufferObjectId > 0)
                    {
                        if (vpVertexBinded)
                        {
                            float size = vertexBucket.LoadingRatio >= 1 ? 1f : (float)Math.Sqrt(1f / vertexBucket.LoadingRatio);
                            size *= pointSizeScale;
                            ShaderHelper.SetParameter(vpVertex, "size", size);
                        }

                        GL.BindBuffer(BufferTarget.ArrayBuffer, vertexBucket.VertexBufferObjectId);
                        GL.InterleavedArrays(InterleavedArrayFormat.C4fN3fV3f, vertexBucket.BytesPerVertex, IntPtr.Zero);
                        if (wallDrawingMethod == WallDrawingMethod.Plane && vertexBucket.Classification == VertexClassification.Wall)
                        {
                            GL.DrawArrays(BeginMode.Lines, 0, vertexBucket.CountInVBO);
                        }
                        else
                        {
                            GL.DrawArrays(BeginMode.Points, 0, vertexBucket.CountInVBO);
                        }
                        GL.BindBuffer(BufferTarget.ArrayBuffer, 0);
                        vertexCount += vertexBucket.CountInVBO;
                    }
                }
            }

            if (bucketId == VertexBucket.WallBucketId && wallDrawingMethod != WallDrawingMethod.Points)
            {
                ShaderHelper.UnbindProgram(ShaderHelper.VertexProfile);
                ShaderHelper.UnbindProgram(ShaderHelper.GeometryProfile);
                ShaderHelper.UnbindProgram(ShaderHelper.FragmentProfile);
            }
            else
            {
                ShaderHelper.UnbindProgram(ShaderHelper.VertexProfile);
                ShaderHelper.UnbindProgram(ShaderHelper.FragmentProfile);
            }
        }

        private static void ComputeViewParameters(out Vector3 eyePosition, out Vector3 lookAtPosition)
        {
            if (AnimationMode == AnimationMode.FlyOver)
            {
                Vector3 distance = (vertexQuadTree.MaxPosition - vertexQuadTree.MinPosition) / 2;
                CameraRotationY = (float)((Math.PI * 2 + CameraRotationY) % (Math.PI * 2));
                if (CameraRotationY < diagonalAngle)
                {
                    eyePosition = new Vector3(
                        (float)(CenterPosition.X + distance.X * Math.Tan(CameraRotationY)),
                        (float)(CenterPosition.Y + CameraHeight),
                        (float)(CenterPosition.Z + distance.Z));
                    lookAtPosition = new Vector3(
                        eyePosition.X,
                        CenterPosition.Y,
                        CenterPosition.Z + distance.Z / 2);

                }
                else if (CameraRotationY < (Math.PI - diagonalAngle))
                {
                    eyePosition = new Vector3(
                        (float)(CenterPosition.X + distance.X),
                        (float)(CenterPosition.Y + CameraHeight),
                        (float)(CenterPosition.Z - distance.Z * Math.Tan(CameraRotationY - Math.PI / 2)));
                    lookAtPosition = new Vector3(
                        CenterPosition.X + distance.X / 2,
                        CenterPosition.Y,
                        eyePosition.Z);
                }
                else if (CameraRotationY < (Math.PI + diagonalAngle))
                {
                    eyePosition = new Vector3(
                        (float)(CenterPosition.X - distance.X * Math.Tan(CameraRotationY - Math.PI)),
                        (float)(CenterPosition.Y + CameraHeight),
                        (float)(CenterPosition.Z - distance.Z));
                    lookAtPosition = new Vector3(
                        eyePosition.X,
                        CenterPosition.Y,
                        CenterPosition.Z - distance.Z / 2);
                }
                else if (CameraRotationY < (Math.PI * 2 - diagonalAngle))
                {
                    eyePosition = new Vector3(
                        (float)(CenterPosition.X - distance.X),
                        (float)(CenterPosition.Y + CameraHeight),
                        (float)(CenterPosition.Z + distance.Z * Math.Tan(CameraRotationY - Math.PI * 3 / 2)));
                    lookAtPosition = new Vector3(
                        CenterPosition.X - distance.X / 2,
                        CenterPosition.Y,
                        eyePosition.Z);
                }
                else
                {
                    eyePosition = new Vector3(
                        (float)(CenterPosition.X + distance.X * Math.Tan(CameraRotationY)),
                        (float)(CenterPosition.Y + CameraHeight),
                        (float)(CenterPosition.Z + distance.Z));
                    lookAtPosition = new Vector3(
                        eyePosition.X,
                        CenterPosition.Y,
                        CenterPosition.Z + distance.Z / 2);
                }
            }
            else
            {
                eyePosition = new Vector3(
                    (float)(CenterPosition.X + CameraDistance * Math.Sin(CameraRotationY)),
                    (float)(CenterPosition.Y + CameraHeight),
                    (float)(CenterPosition.Z + CameraDistance * Math.Cos(CameraRotationY)));
                lookAtPosition = CenterPosition;
            }
        }

        #endregion

    }
}
